זריקה, תפיסה וטיפול בחריגים
ובכלל, למה לי להשתמש ב- try, catch, finally
אקספשנים (או חריגים) הם הדרך שלך לספר למתכנתים אחרים שמשהו הולך לא כפי שציפית ולעשות עם זה משהו
כדי להבין את המשפט למעלה נעבור צעד צעד. כשאני אומר מתכנתים אחרים, אני מתכוון לא רק לחברים לצוות במשרד שלך, אלא גם למי שיראה את הקוד שלך באינטרנט וגם אליך של בעוד חודש, ואפילו אליך של בעוד חמש דקות.
למה לספר להם משהו? אחת המטרות של תכנות מונחה עצמים שדיברנו עליה רבות, היא לעשות את הקוד ברור יותר וקל יותר להבנה. חריגים הם כלי נוסף לעשות את התקשורת בין מתכנתים לקלה יותר.
אז מה כבר יכול ללכת לא בסדר? נניח שכתבת מתודה או פונקציה שקוראת תוכן של קובץ. היא מקבלת נתיב ומחזירה מחרוזת. באמצעות חריגים תוכל לספר למי שהפעיל את המתודה שמשהו לא בסדר, למשל: הנתיב שגוי וקובץ כזה לא קיים, או שהקובץ קיים אבל נעול על ידי תוכנה אחרת.
ברגע שמפעיל המתודה ידע על חגירה ממהלך הביצוע הרגיל של התוכנה הוא יוכל להחליט איך להמשיך הלאה. הוא יכול לבחור ליצור קובץ כזה, יכול לחכות או יכול להציג שגיאה למשתמש ולסיים את הביצוע.
המתרה של חריגים היא לתקן או לשנות כיוון במקרה של חריגה מתהליך הביצוע המתוכנן.
זריקת אקספשנים
אקספשן הוא בסך הכול מחלקה
מחלקת אקספשן היא מחלקה רגילה לחלוטין שמובנית בשפת PHP, ולה יש כמה מאפיינים וכמה מתודות משלה. הדוקומנטציה מביאה את מבנה המחלקה במלואה.
מההגדרה של המחלקה אפשר לראות שלמחלקת אקספשן יש כמה חלקים חשובים:
- הודעת שגיאה
- מספר שגיאה
- שגיאה קודמת
כמה מאפיינים נוספים PHP מכניסה בעצמה בעת יצירת מופע של המחלקה הזו, כמו: מספר שורה, שם הקובץ שבו השגיאה נוצרה ומחסנית הביצוע (רשימת הקריאות למתודות ופונקציות). באמצעות המתודות שמחזירות את המידע הזה, ניתן לדעת בוודאות מה, איפה ובאיזה קובץ הלך לא בסדר.
דוגמה לשימוש במחלקת אקספשן.
לאחר שיצרנו מופע של מחלקת אקספשן, שכולל את כל המידע על הבעיות, עלינו לזרוק אותו למעלה, כדי שמישהו יטפל בו.
זריקת אקספש מתבצעת באמצעות הפקודה throw
אקספשנים, או חריגים, מסמנים חריגה מהליך הביצוע הרגיל של התוכנה. זריקת אקספשן מפסיקה את ביצוע התוכנה איפה שהיא, עד אשר השגיאה תיתפס. אם אף אחד לא יתפוס את השגיאה - השגיאה תפסיק את הביצוע של התוכנה כולה.
throw new \Exception("Huston, we have a ");
השורה הזו תפסיק מיד את ביצוע הקוד איפה שהיא ותעלה למעלה במחסנית הביצוע עד אשר תיתפס.
מחסנית היא אוסף הקריאות והפונקציות המופעלות. הקוד הראשי של התוכנה main קורא לפונקציה a, פונקציה a קוראת לפונקציה b, שבתורה, קוראת לפונקציה c. זוהי מחסנית הקריאות: main -> a -> b -> c. חריג שנזרק מתוך פונקציה c יעצור אותה, יעלה למעלה במחסנית הביצוע ויעצור גם את פונקציה b, לאחר מכן יעלה שוב למעלה ויעצור גם את פונקציית a ואת הביצוע הכללי של הסקריפט. אלא אם כן השגיאה תיתפס. דוגמה לשגיאה שלא נתפסת ועוצרת את כל המחסנית
תפיסת אקספשנים
לפני הכול, מה עושים עם אקספשן שנתפס? יש לא הרבה אפשרויות שונות: לתקן את המצב אם אפשר או לסיים את ביצוע התוכנה. במקרה הראשון נוכל ליצור קובץ שלא קיים, להתחבר מחדש למסד נתונים או ליצור חיבור רשת מחודש. במקרה השני, נעדיף להציג למשתמש הודעת שגיאה ואולי, ובמקרה האידאלי, גם הסבר איך לתקן אותה.
תפיסת שגיאות מתבצעת באמצעות הפקודות try catch
בלוק try catch עוטף ביצוע של קוד ומאפשר לטפל בשגיאות בלי שיצאו מכל מחסנית הביצוע. אקספשן שנזרק ימשיך לעלות במחסנית הביצוע עד אשר ייתפס או עד שיצא מהתוכנה כולה. יציאה מהתוכנה כולה היא לא תוצאה רצויה, לעומת זאת הצגת הודעת שגיאה סבירה תהיה עדיפה בהרבה.
try
{
throw new \Exception("Oh noez");
}
catch(\Exception $ex)
{
echo 'oi, an exception has happened: ', $ex->getMessage();
}
{
throw new \Exception("Oh noez");
}
catch(\Exception $ex)
{
echo 'oi, an exception has happened: ', $ex->getMessage();
}
בפועל, הקוד שיהיה בתוך פקודת ה-try יהיה קצת יותר מסובך מהקוד בדוגמה. בדרך כלל הוא יכיל קריאות לפונקציות, הפעלה של מתודות שיפעילו מתודות אחרות, או כל דבר אחר. שגיאה שתיזרק מתוך עמקי הקוד שעטוף ב-try בסופו של דבר תגיע אל בלוק ה-catch.
בתוך קוד ה-catch אפשר לעשות פעולות שונות: להפעיל את אותה הפעולה מחדש ברקורסיה, לתקן משהו, או פשוט לרשום ללוג שמשהו הלך לא בסדר ולסיים את הביצוע. יכול להיות שהשגיאה לא קריטית ואפשר להמשיך הלאה.
בלוק ה-catch מכיל בסוגריים את סוג השגיאה שאמורה להיתפס. עד עכשיו זרקנו מופע של מחלקת \Exception. היות שזו היא בסה"כ מחלקה - אפשר לרשת אותה וליצור תת-מחלקות שלה עם שגיאה מדויקת יותר או פירוט מתאים יותר.
הרחבת מחלקת אקספשן
את מחלקת אקספשן ניתן לרשת. יותר מזה, ב-PHP קיימות כמה מחלקות מובנות שיורשות את מחלקת \Exception. במקרה של אקספשנים המטרה של ירושה היא הוספת דיוק וספציפיות. במקרה של הקובץ שלא נמצא, נוכל ליצור תת-מחלקה שמייצגת שגיאה של קובץ שלא נמצא. תת-מחלקה כזו יכולה להכיל מאפיין של נתיב הקובץ במקום הודעת השגיאה.
class FileNotFoundException extends \Exception
{
private $path;
public function __construct($path)
{
$this->path = $path;
parent::__construct("File $path was not found");
}
public function GetPath()
{
return $this->path;
}
}
{
private $path;
public function __construct($path)
{
$this->path = $path;
parent::__construct("File $path was not found");
}
public function GetPath()
{
return $this->path;
}
}
המטרה העיקרית של מחלקה הזו היא לא הוספת מאפניינים, אלא דיוק בסוג השגיאה. קוד שיתפוס או מתכנת שיראה מופע של מחלקת FileNotFoundException מיד יבין בצורה הרבה יותר מדויקת מה היא השגיאה. מעבר לזה, קוד אחד יכול להחזיר כמה שגיאות שונות והטיפול בהם יכול להיות שונה, לכן רצו להחזיר מופע של מחלקה כמה שיותר ספציפית.
בוא נראה איך לתפוס שגיאה מסוג מסוים:
class NumberTooBig extends \Exception {}
class NumberTooSmall extends \Exception {}
function Boo($number)
{
if($number < 100)
throw new NumberTooSmall();
if($number > 2000)
throw new NumberTooBig();
echo 'Great number';
}
try
{
$number = 4300; // _GET[]
Boo($number);
}
catch(NumberTooBig $ex)
{
echo "You've given a number too big.";
}
catch(NumberTooSmall $ex)
{
echo "Your number is way to small";
}
catch(\Exception $ex)
{
echo "Unknown error";
}
class NumberTooSmall extends \Exception {}
function Boo($number)
{
if($number < 100)
throw new NumberTooSmall();
if($number > 2000)
throw new NumberTooBig();
echo 'Great number';
}
try
{
$number = 4300; // _GET[]
Boo($number);
}
catch(NumberTooBig $ex)
{
echo "You've given a number too big.";
}
catch(NumberTooSmall $ex)
{
echo "Your number is way to small";
}
catch(\Exception $ex)
{
echo "Unknown error";
}
שים לב לטיפול השונה בשני המקרים. ביצוע אותו הקוד יכול להחזיר שגיאות שונות, בעיה בחיבור למסד נתונים, קלט לא תקין, נגמר הזיכרון בשרת וכו'. תת-מחלקות שונות של מחלקת אקספשן מאפשרות לעשות את הקוד יותר מדויק ולטפל בצורה שונה בבעיות שונות.
שים לב לבלוק ה-catch האחרון בדוגמה הזו. כל שגיאה שלא נתפסה לפני כן, תיתפס על ידי הבלוק הזה. אם השגיאה שנזרקה היא לא מאף טיפוס ספציפי שאנחנו מנסים לתפוס, הבלוק האחרון יתפוס אותה.
לבסוף finally
קוד שזורק שגיאה מופסק באמצע. ברוב המקרים הפסקת קוד באמצע יכולה להשאיר אותנו במצב לא תקין, חיבור רשת או מסד פתוח, רק חצי תוכן שנרשם לקובץ או מצב לא נוח אחר. בלוק finally הוא בלוק קוד שמובטח להתבצע בכל מקרה, בין אם קרתה שגיאה או בין אם הכל הלך בסדר. בלוק קוד כזה צריך להביא את המערכת למצב סופי כלשהו (על כן השם finally), כמו סגירת חיבור, שמירת קובץ או ניקוי משאבים אחרים.
השימוש העיקרי של finally הוא ניקוי משאבים והוא נוסף לשפת php החל מגרסת 5.5. בהרבה מקרים אין צורך בניקוי משאבים, היות ש-PHP תנקה את המשאבים אחרי הסקריפט בכל מקרה, אבל עדיף לכתוב קוד תקין מאשר להסתמך על PHP ועל זה, שמישהו אחר ישתמש בקוד שלך באתר ולא כחלק מתוכנת שורת פקודה אינסופית.
דוגמה ל-finally:
try
{
$fp = fsockopen("phpguide.co.il", 80, $errno, $errstr, 30);
// ...
}
catch(\Exception $ex)
{
echo 'Bad day for socketing';
}
finally
{
if(is_resource($fp))
fclose($fp);
}
{
$fp = fsockopen("phpguide.co.il", 80, $errno, $errstr, 30);
// ...
}
catch(\Exception $ex)
{
echo 'Bad day for socketing';
}
finally
{
if(is_resource($fp))
fclose($fp);
}
בדוגמה הזו בלוק ה-finally וניתוק החיבור יתבצעו תמיד, בין אם הייתה שגיאה ובין אם לא.
Best Practices
הנה רשימה קצרה של best practices שאני מוצא לנכון לשמור עליהם כשאתה כותב קוד שאתה בעצמך הולך להשתמש בו, ולשמור עליהם עוד יותר כשאתה כותב ספריה שמיועדת לשימוש על ידי מתכנתים אחרים.
צור והחזר שגיאות ספציפיות ככל האפשר
צור תת-מחלקות של מחלקת אקספשן עבור כל סוג שגיאה אפשרי שהקוד שלך יכול להחזיר. ככל שיהיו לך יותר טיפוסים שונים, ככה הם יהיו יותר ברורים ויהיה קל יותר לטפל בהם. אין שום בעיה אם במערכת שלך מוגדרות 50 מחלקות שגיאות שונות. רק אחת מהן תיטען ותיזרק במקרה שמשהו ילך לא בסדר.
החזר בהודעת השגיאה גם מידע איך לתקן אותה
הודעה לא ברורה:
Invalid API key
הודעה ברורה:
Invalid API key. Please make sure the parameter $apiKey passed is correct. You have passed the string "", but expected at least 15 characters. You can obtain your api key from example.com/keys
עטוף את כל הקוד שלך ב-try catch
גם אם אתה לא רוצה לבצע נתיב פעולות חלופי במקרה של שגיאה, עדיף לך להציג הודעת שגיאה יפה למשתמש שלא כוללת פרטי התחברות למסד, ולא לשכוח ליידע את המתכנתים על זה, שמשהו הלך לא כשורה. במדריכים אחרים כבר דיברנו על חשיבות של טיפול בשגיאות וחריגים הם שגיאות לכל דבר.
תפוס אקספשן בשלב הכי מוקדם שאפשר
אקספשן שלא נתפס עולה למעלה במחסנית. ככל שיותר מוקדם תתפוס אותו, כך פחות קוד אקספשן ימנע מלהתבצע. במקרה שבו אתה יכול לתקן את הליך הביצוע - תרצה לעשות את זה ברמה הכי פנימית של מחסנית הביצוע, בלי שהאקספשן יחלחל למעלה ויפסיק חלקים אחרים של התוכנה, כמו הצגת עמוד. כלומר, בעיה עם קובץ שלא קיים תעדיף לפתור בתוך בלוק try/catch שנמצא בפונקציה לקריאת קבצים ולא ב-try/catch הכללי שנמצא ב-index.php.
רשום אקספשן ללוג רק פעם אחת
רישום של אקספשן ללוג צריך להתבצע רק פעם אחת במקום שבו התוכנה מחליטה מה לעשות איתו, לתקן או להפסיק את הביצוע לגמרי. אקספשן שנזרק על קובץ לא קיים צריך להירשם ללוג במקום, שבו התוכנה מחליטה מה לעשות איתו, למשל במחלקת העבודה עם קבצים, ולא במחלקת ההרשמה שמפעילה עבודה עם קבצים.
הפרד טיפול בשגיאות מלוגיקה
האות S שמשמעה Single Responsibilily בעיקרון ה-SOLID שאנחנו מדברים עליו בספר תכנות מונחה עצמים מאפס אומר שלכל מחלקה ולכל מתודה צריכה להיות דאגה אחת או עיסוק אחד בלבד. העיקרון הזה נכון גם עבור אקספשנים. טיפול בשגיאות וביצוע ביזנס לוגיקה הם דברים שונים ולכן כדאי להפריד אותם. מתודה שבה תשים try/catch צריכה להפעיל מתודה אחרת שבה קורית הביזנס לוגיקה. אין צורך לערבב את כל הקוד בפונקציה אחת. הנה דוגמה להפרדת של מתודות.
תגובות לכתבה:
יישר כוח על המדריך. לא יצא לי עדיין לראות את הנושא הנ"ל מכוסה בעברית.